﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using NetworkUI;
using Red.Controls.Diagrams.Declarators;
using Red.Controls.Diagrams.Dummy;
using Red.Controls.Diagrams.InteropUtils;
using Red.Controls.Diagrams.ViewModels;
using Red.Controls.ViewModels;
using Red.EngineData.View;
using Xceed.Wpf.Toolkit.Core.Input;

namespace Red.Controls.Diagrams
{
    /// <summary>
    /// List of internal class names that are handled differently than standard nodes.
    /// </summary>
    internal static class InternalClassNames
    {
        public const string MissingDeclarator = "MissingDeclarator";
    }

    /// <summary>
    /// Interaction logic for NodeControl.xaml
    /// </summary>
    public partial class NodeControl : UserControl
    {
        #region Data

        private string _declaratorsPath;
        private readonly CommandPalette _commandPalette = new CommandPalette();
        private IObjectManager _nodeManager;

        #endregion Data

        #region Properties

        public DiagramViewModel ViewModel => ( DiagramViewModel )DataContext;

        public string DeclaratorsPath
        {
            set
            {
                if ( _declaratorsPath == value )
                    return;

                _declaratorsPath = value;
                Declarators.Load( _declaratorsPath );
            }
            get { return _declaratorsPath; }
        }

        /// <summary>
        /// Orientation of nodes in the control.
        /// Horizontal nodes have inputs on the left side and outputs on the right side.
        /// Vertical nodes have inputs on top and outputs on the bottom.
        /// This value will not be changing on runtime.
        /// </summary>
        public NodeOrientation Orientation
        {
            get { return ViewModel.Orientation; }
            set { ViewModel.Orientation = value; }
        }

        public IEnumerable<NodeViewModel> SelectedNodes
        {
            get { return ViewModel.Network.Nodes.Where( node => node.IsSelected ); }
        }

        public NodeDeclarators Declarators { get; } = new NodeDeclarators();

        #endregion Properties

        #region C-tor

        public NodeControl()
        {
            InitializeComponent();
            InitZoombox();
            Loaded += OnLoaded;
            Breadcrumb.Path = @"{,icon:appbar_home}";
        }

        private void InitZoombox()
        {
            _zoombox.DragModifiers.Clear();
            _zoombox.DragModifiers.Add( KeyModifier.Exact );
            _zoombox.ZoomToSelectionModifiers.Clear();
            _zoombox.ZoomToSelectionModifiers.Add( KeyModifier.Blocked );
            _zoombox.RelativeZoomModifiers.Clear();
            _zoombox.RelativeZoomModifiers.Add( KeyModifier.Exact );
            _zoombox.Cursor = Cursors.Arrow;
            _zoombox.IsAnimated = false;
            _zoombox.ViewModel = ViewModel;
        }

        #endregion C-tor

        #region Public methods

        public async Task LoadAsync( DataView rootEngineObject )
        {
            _nodeManager = new InteropObjectManager( rootEngineObject, ViewModel, Declarators );
            var stringGuid = "";
            await _nodeManager.CreateDiagramAsync( stringGuid );
            var mainPath = $@"{{,{stringGuid},icon:appbar_home}}";
            Breadcrumb.Path = mainPath;
            _zoombox.ZoomToNodesBounds();
        }

        /// <summary>
        /// Used to edit NodeDeclarators. No data behind.
        /// </summary>
        /// <param name="declarator"></param>
        public async Task LoadDummyAsync( NodeDeclarator declarator )
        {
            _nodeManager = new DummyObjectManager( ViewModel, Declarators, declarator );
            await _nodeManager.CreateDiagramAsync( null );
            _zoombox.ZoomToNodesBounds();
        }

        public bool CanCopySelectedNodes()
        {
            return _nodeManager != null && _nodeManager.CanCopySelectedNodes();
        }

        public void CopySelectedNodes()
        {
            _nodeManager.CopySelectedNodes();
        }

        public void CutSelectedNodes()
        {
            _nodeManager.CutSelectedNodes();
        }

        public bool CanPasteNodes()
        {
            return _nodeManager != null && _nodeManager.CanPasteNodes();
        }

        public void PaseteNodes()
        {
            var position = Mouse.GetPosition( _networkView );
            _nodeManager.PasteNodes( position );
        }

        #endregion Public methods

        #region Private methods

        private void OnLoaded( object sender, RoutedEventArgs routedEventArgs )
        {
            var window = Window.GetWindow( this );
            if ( window != null )
                window.Closing += ( s, a ) => _commandPalette.Close();
        }

        private bool PopulateCommandPalette( Point mousePositionInScreenCoordinates )
        {
            var declarators = Declarators.GetDeclarators();
            var items = new List<CommandPaletteItem>();

            foreach ( var declarator in declarators )
            {
                // do not show abstract classes in command palette
                if ( declarator.IsAbstract )
                    continue;

                var className = declarator.Class;
                // do not show internal classes in command palette
                if ( IsInternalClassName( className ) )
                    continue;

                var commandPaletteItem = new CommandPaletteItem<PaletteNodeCreator>( new PaletteNodeCreator( mousePositionInScreenCoordinates, declarator ) )
                {
                    Name = declarator.Class + " (" + declarator.Title + ")",
                    Comment = declarator.Title, // TODO: maybe in future we should add short descriptions to the declarators. That descriptions could be displayed in each command palette item tooltip.
                };

                commandPaletteItem.Click += CommandPaletteItem_Click;
                items.Add( commandPaletteItem );
            }

            _commandPalette.Items = items.ToList();
            return items.Any();
        }

        private bool PopulateCommandPaletteForConnectedNodes( Point mousePositionInScreenCoordinates, string requestedClassName, ConnectionViewModel connection )
        {
            var declarators = Declarators.GetDeclarators( requestedClassName );
            var items = new List<CommandPaletteItem>();

            foreach ( var declarator in declarators )
            {
                // do not show abstract classes in command palette
                if ( declarator.IsAbstract )
                    continue;

                var commandPaletteItem = new CommandPaletteItem<PaletteConnectedNodeCreator>( new PaletteConnectedNodeCreator( mousePositionInScreenCoordinates, declarator, connection ) )
                {
                    Name = declarator.Class + " (" + declarator.Title + ")",
                    Comment = declarator.Title, // TODO: maybe in future we should add short descriptions to the declarators. That descriptions could be displayed in each command palette item tooltip.
                };

                commandPaletteItem.Click += CommandPaletteItemWithConnection_Click;
                items.Add( commandPaletteItem );
            }

            _commandPalette.Items = items.ToList();
            return items.Any();
        }

        private void CommandPaletteItemWithConnection_Click( object sender, PaletteConnectedNodeCreator e )
        {
            var position = _networkView.PointFromScreen( e.Position );
            _nodeManager.CreateConnectedObject( e.Declarator.Class, position, e.Connection );
        }

        private bool PopulateCommandPaletteForConnection( Point mousePositionInScreenCoordinates, ConnectionViewModel connection )
        {
            var items = new List<CommandPaletteItem>();

            var sourceType = connection.SourceConnector.Property.GetType();
            var destType = connection.DestConnector.Property.GetType();
            var compatibleDeclarators = Declarators.GetDeclarators( sourceType ).Where( item => !item.IsAbstract && item.Outputs.Count != 0 );

            foreach ( var declarator in compatibleDeclarators )
            {
                if ( !_nodeManager.ObjectHasOutputOfType( declarator.Class, destType ) )
                    continue;

                var commandPaletteItem = new CommandPaletteItem<PaletteConnectedNodeCreator>( new PaletteConnectedNodeCreator( mousePositionInScreenCoordinates, declarator, connection ) )
                {
                    Name = declarator.Class + " (" + declarator.Title + ")",
                    Comment = declarator.Title, // TODO: maybe in future we should add short descriptions to the declarators. That descriptions could be displayed in each command palette item tooltip.
                };

                commandPaletteItem.Click += CommandPaletteDecorateConnection_Click;
                items.Add( commandPaletteItem );
            }

            _commandPalette.Items = items.ToList();
            return items.Any();
        }

        private void CommandPaletteDecorateConnection_Click( object sender, PaletteConnectedNodeCreator e )
        {
            var position = _networkView.PointFromScreen( e.Position );
            _nodeManager.CreateDecoratorObject( e.Declarator.Class, position, e.Connection );
        }

        private void CreateConnectedNode( ConnectionViewModel connection, ConnectorViewModel connector )
        {
            if ( !ViewModel.IsEditMode )
                return;

            var type = connector.Property.GetType();
            var position = GetMousePositionInScreenCoordinates();
            _commandPalette.Hide();
            _commandPalette.Top = position.Y;
            _commandPalette.Left = position.X;
            if ( PopulateCommandPaletteForConnectedNodes( position, type, connection ) )
            {
                _commandPalette.Hiding += CommandPalette_Hiding;
                _commandPalette.Show( connection );
                _commandPalette.ResetInitialState();
            }
        }

        private bool IsInternalClassName( string name )
        {
            if ( name == InternalClassNames.MissingDeclarator )
                return true;

            return false;
        }

        private Point GetMousePositionInScreenCoordinates()
        {
            var position = Mouse.GetPosition( _networkView );
            return _networkView.PointToScreen( position );
        }

        #endregion Private methods

        #region Events

        /// <summary>
        /// An event raised when the nodes selected in the NetworkView has changed.
        /// </summary>
        public event SelectionChangedEventHandler SelectionChanged;
        private void OnSelectionChanged( SelectionChangedEventArgs e )
        {
            if ( SelectionChanged != null )
                SelectionChanged( this, e );               
        }

        #endregion Events

        #region Event handlers

        private void ToggleConnection_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var connectionViewModel = e.Parameter as ConnectionViewModel;
            if ( connectionViewModel == null )
                return;

            connectionViewModel.IsActive = !connectionViewModel.IsActive;
        }

        private void DecorateConnection_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var connectionViewModel = e.Parameter as ConnectionViewModel;
            if ( connectionViewModel == null )
                return;

            var position = GetMousePositionInScreenCoordinates();
            _commandPalette.Hide();
            _commandPalette.Top = position.Y;
            _commandPalette.Left = position.X;
            if ( PopulateCommandPaletteForConnection( position, connectionViewModel ) )
            {
                _commandPalette.Show();
                _commandPalette.ResetInitialState();
            }
        }

        private void NetworkView_ConnectionDragStarted( object sender, ConnectionDragStartedEventArgs e )
        {
            var draggedOutConnector = ( ConnectorViewModel )e.ConnectorDraggedOut;
            var curDragPoint = Mouse.GetPosition( _networkView );

            //
            // Delegate the real work to the view model.
            //
            var connection = ViewModel.ConnectionDragStarted( draggedOutConnector, curDragPoint );

            //
            // Must return the view-model object that represents the connection via the event args.
            // This is so that NetworkView can keep track of the object while it is being dragged.
            //
            e.Connection = connection;
        }

        private void NetworkView_ConnectionDragging( object sender, ConnectionDraggingEventArgs e )
        {
            var curDragPoint = Mouse.GetPosition( _networkView );
            var connection = ( ConnectionViewModel )e.Connection;
            ViewModel.ConnectionDragging( curDragPoint, connection );
        }

        private void NetworkView_ConnectionDragCompleted( object sender, ConnectionDragCompletedEventArgs e )
        {
            var connectorDraggedOut = ( ConnectorViewModel )e.ConnectorDraggedOut;
            var connectorDraggedOver = ( ConnectorViewModel )e.ConnectorDraggedOver;
            var newConnection = ( ConnectionViewModel )e.Connection;

            var inputConnector = connectorDraggedOut.Type == ConnectorType.Input
                ? connectorDraggedOut
                : connectorDraggedOver;

            var outputConnector = connectorDraggedOut.Type == ConnectorType.Output
                ? connectorDraggedOut
                : connectorDraggedOver;

            if ( outputConnector != null && inputConnector == null )
            {
                // connection dragged from output connector onto background
                CreateConnectedNode( newConnection, outputConnector );
            }
            else if ( outputConnector == null )
            {
                // connection dragged from input connector onto background, dismiss it
                ViewModel.DeleteConnection( newConnection );
            }
            else if ( outputConnector == inputConnector )
            {
                // connection dragged from to the same connector
                ViewModel.DeleteConnection( newConnection );
            }
            else
            {
                // connection dragged between two connectors
                _nodeManager.CreateConnection( connectorDraggedOut, connectorDraggedOver, newConnection );
            }
        }

        private void CommandPalette_Hiding( CommandPalette sender, CommandPaletteEventArgs args )
        {
            sender.Hiding -= CommandPalette_Hiding;
            if ( !args.Result )
            {
                var connection = args.Argument as ConnectionViewModel;
                ViewModel.DeleteConnection( connection );
            }
        }

        private async void NetworkView_QueryConnectionFeedback( object sender, QueryConnectionFeedbackEventArgs e )
        {
            var connectorDraggedOut = ( ConnectorViewModel )e.ConnectorDraggedOut;
            var connectorDraggedOver = ( ConnectorViewModel )e.DraggedOverConnector;
            var connectionValid = await ViewModel.QueryConnectionFeedbackAsync( connectorDraggedOut, connectorDraggedOver );

            var connection = ( ConnectionViewModel ) e.Connection;
            connection.IsValid = connectionValid;
            //
            // Let NetworkView know if the connection is ok or not ok.
            //
            e.ConnectionOk = connectionValid;
        }

        private void AddNode_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            e.CanExecute = ViewModel.Network != null && ViewModel.IsEditMode;
        }

        private void AddNode_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var mousePositionInScreenCoordinates = GetMousePositionInScreenCoordinates();
            _commandPalette.Hide();

            _commandPalette.Top = mousePositionInScreenCoordinates.Y;
            _commandPalette.Left = mousePositionInScreenCoordinates.X;
            if ( PopulateCommandPalette( mousePositionInScreenCoordinates ) )
            {
                _commandPalette.Show();
                _commandPalette.ResetInitialState();
            }
        }

        private void CommandPaletteItem_Click( object sender, PaletteNodeCreator e )
        {
            var position = e.Position;
            var nodeDeclarator = e.Declarator;
            position = _networkView.PointFromScreen( position );
            _nodeManager.CreateObject( nodeDeclarator.Class, position );
        }

        private void BackgroundClick_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            _commandPalette.Hide();
        }

        private void NodeDoubleClick_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            var node = e.Parameter as NodeViewModel;
            e.CanExecute = node?.IsContainer ?? false;
        }

        private async void NodeDoubleClick_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var node = e.Parameter as NodeViewModel;
            Breadcrumb.Path += $@"{{{node.Name},{node.ID}}}";
            await _nodeManager.CreateDiagramAsync( node.ID );
            _zoombox.ZoomToNodesBounds();
        }

        private void DeleteSelectedNodes_CanExecute( object sender, CanExecuteRoutedEventArgs e )
        {
            if ( ViewModel.Network == null || !ViewModel.IsEditMode )
            {
                e.CanExecute = false;
                return;
            }

            if ( ViewModel.Network.Nodes.Count == 0 )
            {
                e.CanExecute = false;
                return;
            }

            var isAnyNodeSelected = ViewModel.Network.Nodes.Any( item => item.IsSelected );
            e.CanExecute = isAnyNodeSelected;
        }

        private void DeleteSelectedNodes_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            _nodeManager.DeleteSelectedNodes();
        }

        private void DeleteConnection_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var connectionToRemove = e.Parameter as ConnectionViewModel;
            _nodeManager.DeleteConnection( connectionToRemove );
        }

        private void DeleteAttachedConnections_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            var connector = e.Parameter as ConnectorViewModel;
            var connectionsToRemove = connector.AttachedConnections.ToList();

            _nodeManager.DeleteConnections( connectionsToRemove );
        }

        private void ShowEntireDiagram_Executed( object sender, ExecutedRoutedEventArgs e )
        {
            _zoombox.ZoomToNodesBounds();
        }

        /// <summary>
        /// Callback fired when we click on item in BreadcrumbBar.
        /// </summary>
        private async void Breadcrumb_NavigateTo( object sender, BreadcrumbBarItem e )
        {
            await _nodeManager.CreateDiagramAsync( e.PathElement );
            _zoombox.ZoomToNodesBounds();
        }

        private void NetworkView_SelectionChanged( object sender, SelectionChangedEventArgs e )
        {
            OnSelectionChanged( e );
        }

        private void NetworkView_NodeDragStarted( object sender, NodeDragStartedEventArgs e )
        {
            var nodeDragger = new NodeDragger( _networkView, e.Nodes.Cast<NodeViewModel>() );
            nodeDragger.Finished += NodeDragger_Finished;
        }

        private void NodeDragger_Finished( NodeDragger sender, IEnumerable<Tuple<double, double, string, NodeViewModel>> nodesDataTuple )
        {
            sender.Finished -= NodeDragger_Finished;
            _nodeManager.MoveNodes( nodesDataTuple );
        }

        #endregion Event handlers

        #region Mouse handlers

        /// <summary>
        /// This is handled here and passed to networkView as the latter wouldn't handle this event because of a different size.
        /// </summary>
        protected override void OnMouseDown( MouseButtonEventArgs e )
        {
            //Disabling mouse down event in NodeDeclaratorEditor
            if ( !ViewModel.IsEditMode )
            {
                e.Handled = true;
                return;
            }

            base.OnMouseDown( e );
            _networkView.HandleMouseDown( e );
        }

        /// <summary>
        /// This is handled here and passed to networkView as the latter wouldn't handle this event because of a different size.
        /// </summary>
        protected override void OnMouseUp( MouseButtonEventArgs e )
        {
            //Disabling mouse up event in NodeDeclaratorEditor
            if ( !ViewModel.IsEditMode )
            {
                e.Handled = true;
                return;
            }

            base.OnMouseUp( e );
            _networkView.HandleMouseUp( e );
        }

        /// <summary>
        /// This is handled here and passed to networkView as the latter wouldn't handle this event because of different size.
        /// </summary>
        protected override void OnMouseMove( MouseEventArgs e )
        {
            base.OnMouseMove( e );
            _networkView.HandleMouseMove( e );
        }

        #endregion Mouse handlers
    }

    internal class NodeViewModelSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate( object item, DependencyObject container )
        {
            var element = container as FrameworkElement;
            if ( element == null )
                return base.SelectTemplate( item, container );

            var nodeViewModel = item as NodeViewModel;
            if ( nodeViewModel == null )
                return base.SelectTemplate( item, container );

            string key;
            switch ( nodeViewModel.VisualType )
            {
                case NodeVisualType.Rect:
                case NodeVisualType.RoundRect:
                    key = "RectangleShape";
                    break;
                default:
                    key = "UnknownNodeShape";
                    break;
            }

            return ( DataTemplate )element.TryFindResource( key );
        }
    }

    internal class PaletteNodeCreator
    {
        public Point Position { get; private set; }
        public NodeDeclarator Declarator { get; private set; }

        public PaletteNodeCreator( Point position, NodeDeclarator declarator )
        {
            Position = position;
            Declarator = declarator;
        }
    }

    internal class PaletteConnectedNodeCreator : PaletteNodeCreator
    {
        public ConnectionViewModel Connection { get; set; }

        public PaletteConnectedNodeCreator( Point position, NodeDeclarator declarator, ConnectionViewModel connection )
            : base( position, declarator )
        {
            Connection = connection;
        }
    }

    public class ExecuteCommand : TriggerAction<DependencyObject>
    {
        public ICommand Command
        {
            get { return ( ICommand )GetValue( CommandProperty ); }
            set { SetValue( CommandProperty, value ); }
        }

        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register( "Command", typeof( ICommand ), typeof( ExecuteCommand ), null );

        public object CommandParameter
        {
            get { return ( object )GetValue( CommandParameterProperty ); }
            set { SetValue( CommandParameterProperty, value ); }
        }

        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register( "CommandParameter", typeof( object ), typeof( ExecuteCommand ), null );

        public UIElement CommandTarget
        {
            get { return ( UIElement )GetValue( CommandTargetProperty ); }
            set { SetValue( CommandTargetProperty, value ); }
        }

        public static readonly DependencyProperty CommandTargetProperty =
            DependencyProperty.Register( "CommandTarget", typeof( UIElement ), typeof( ExecuteCommand ), null );

        protected override void Invoke( object parameter )
        {
            if ( Command is RoutedCommand )
            {
                var routedCommand = Command as RoutedCommand;
                var commandTarget = CommandTarget ?? AssociatedObject as UIElement;
                if ( routedCommand.CanExecute( CommandParameter, commandTarget ) )
                    routedCommand.Execute( CommandParameter, commandTarget );
            }
            else
            {
                if ( Command.CanExecute( CommandParameter ) )
                    Command.Execute( CommandParameter );
            }
        }
    }
}
